#include "wx/wxprec.h"
#if wxUSE_FS_ARCHIVE

#include "wx/fs_arc.h"

#ifndef WXPRECOMP
#include "wx/intl.h"
#include "wx/log.h"
#endif

#include "wx/zipstrm.h"
#include "wx/private/fileback.h"

WX_DECLARE_STRING_HASH_MAP( wxArchiveEntry*, wxArchiveFSEntryHash );

struct wxArchiveFSEntry {
  wxArchiveEntry *entry;
  wxArchiveFSEntry *next;
};

class wxArchiveFSCacheDataImpl {
  public:
    wxArchiveFSCacheDataImpl( const wxArchiveClassFactory& factory,
                              const wxBackingFile& backer );
    wxArchiveFSCacheDataImpl( const wxArchiveClassFactory& factory,
                              wxInputStream *stream );

    ~wxArchiveFSCacheDataImpl();

    void Release() { if( --m_refcount == 0 ) delete this; }
    wxArchiveFSCacheDataImpl *AddRef() { m_refcount++; return this; }

    wxArchiveEntry *Get( const wxString& name );
    wxInputStream *NewStream() const;

    wxArchiveFSEntry *GetNext( wxArchiveFSEntry *fse );

  private:
    wxArchiveFSEntry *AddToCache( wxArchiveEntry *entry );
    void CloseStreams();

    int m_refcount;

    wxArchiveFSEntryHash m_hash;
    wxArchiveFSEntry *m_begin;
    wxArchiveFSEntry **m_endptr;

    wxBackingFile m_backer;
    wxInputStream *m_stream;
    wxArchiveInputStream *m_archive;
};

wxArchiveFSCacheDataImpl::wxArchiveFSCacheDataImpl(
  const wxArchiveClassFactory& factory,
  const wxBackingFile& backer )
  :  m_refcount( 1 ),
     m_begin( NULL ),
     m_endptr( &m_begin ),
     m_backer( backer ),
     m_stream( new wxBackedInputStream( backer ) ),
     m_archive( factory.NewStream( *m_stream ) ) {
}

wxArchiveFSCacheDataImpl::wxArchiveFSCacheDataImpl(
  const wxArchiveClassFactory& factory,
  wxInputStream *stream )
  :  m_refcount( 1 ),
     m_begin( NULL ),
     m_endptr( &m_begin ),
     m_stream( stream ),
     m_archive( factory.NewStream( *m_stream ) ) {
}

wxArchiveFSCacheDataImpl::~wxArchiveFSCacheDataImpl() {
  WX_CLEAR_HASH_MAP( wxArchiveFSEntryHash, m_hash );
  wxArchiveFSEntry *entry = m_begin;
  while( entry ) {
    wxArchiveFSEntry *next = entry->next;
    delete entry;
    entry = next;
  }
  CloseStreams();
}

wxArchiveFSEntry *wxArchiveFSCacheDataImpl::AddToCache( wxArchiveEntry *entry ) {
  m_hash[entry->GetName( wxPATH_UNIX )] = entry;
  wxArchiveFSEntry *fse = new wxArchiveFSEntry;
  *m_endptr = fse;
  ( *m_endptr )->entry = entry;
  ( *m_endptr )->next = NULL;
  m_endptr = &( *m_endptr )->next;
  return fse;
}

void wxArchiveFSCacheDataImpl::CloseStreams() {
  delete m_archive;
  m_archive = NULL;
  delete m_stream;
  m_stream = NULL;
}

wxArchiveEntry *wxArchiveFSCacheDataImpl::Get( const wxString& name ) {
  wxArchiveFSEntryHash::iterator it = m_hash.find( name );
  if( it != m_hash.end() ) {
    return it->second;
  }
  if( !m_archive ) {
    return NULL;
  }
  wxArchiveEntry *entry;
  while( ( entry = m_archive->GetNextEntry() ) != NULL ) {
    AddToCache( entry );
    if( entry->GetName( wxPATH_UNIX ) == name ) {
      return entry;
    }
  }
  CloseStreams();
  return NULL;
}

wxInputStream* wxArchiveFSCacheDataImpl::NewStream() const {
  if( m_backer ) {
    return new wxBackedInputStream( m_backer );
  } else
  { return NULL; }
}

wxArchiveFSEntry *wxArchiveFSCacheDataImpl::GetNext( wxArchiveFSEntry *fse ) {
  wxArchiveFSEntry *next = fse ? fse->next : m_begin;
  if( !next && m_archive ) {
    wxArchiveEntry *entry = m_archive->GetNextEntry();
    if( entry ) {
      next = AddToCache( entry );
    } else
    { CloseStreams(); }
  }
  return next;
}

//---------------------------------------------------------------------------
// wxArchiveFSCacheData
//
// This is the inteface for wxArchiveFSCacheDataImpl above. Holds the catalog
// of an archive file, and if it is being read from a non-seekable stream, a
// copy of its backing file.
//---------------------------------------------------------------------------

class wxArchiveFSCacheData {
  public:
    wxArchiveFSCacheData() : m_impl( NULL ) { }
    wxArchiveFSCacheData( const wxArchiveClassFactory& factory,
                          const wxBackingFile& backer );
    wxArchiveFSCacheData( const wxArchiveClassFactory& factory,
                          wxInputStream *stream );

    wxArchiveFSCacheData( const wxArchiveFSCacheData& data );
    wxArchiveFSCacheData& operator=( const wxArchiveFSCacheData& data );

    ~wxArchiveFSCacheData() { if( m_impl ) m_impl->Release(); }

    wxArchiveEntry *Get( const wxString& name ) { return m_impl->Get( name ); }
    wxInputStream *NewStream() const { return m_impl->NewStream(); }
    wxArchiveFSEntry *GetNext( wxArchiveFSEntry *fse ) { return m_impl->GetNext( fse ); }

  private:
    wxArchiveFSCacheDataImpl *m_impl;
};

wxArchiveFSCacheData::wxArchiveFSCacheData(
  const wxArchiveClassFactory& factory,
  const wxBackingFile& backer )
  : m_impl( new wxArchiveFSCacheDataImpl( factory, backer ) ) {
}

wxArchiveFSCacheData::wxArchiveFSCacheData(
  const wxArchiveClassFactory& factory,
  wxInputStream *stream )
  : m_impl( new wxArchiveFSCacheDataImpl( factory, stream ) ) {
}

wxArchiveFSCacheData::wxArchiveFSCacheData( const wxArchiveFSCacheData& data )
  : m_impl( data.m_impl ? data.m_impl->AddRef() : NULL ) {
}

wxArchiveFSCacheData& wxArchiveFSCacheData::operator=(
  const wxArchiveFSCacheData& data ) {
  if( data.m_impl != m_impl ) {
    if( m_impl ) {
      m_impl->Release();
    }
    m_impl = data.m_impl;
    if( m_impl ) {
      m_impl->AddRef();
    }
  }
  return *this;
}

//---------------------------------------------------------------------------
// wxArchiveFSCache
//
// wxArchiveFSCacheData caches a single archive, and this class holds a
// collection of them to cache all the archives accessed by this instance
// of wxFileSystem.
//---------------------------------------------------------------------------

WX_DECLARE_STRING_HASH_MAP( wxArchiveFSCacheData, wxArchiveFSCacheDataHash );

class wxArchiveFSCache {
  public:
    wxArchiveFSCache() { }
    ~wxArchiveFSCache() { }

    wxArchiveFSCacheData* Add( const wxString& name,
                               const wxArchiveClassFactory& factory,
                               wxInputStream *stream );

    wxArchiveFSCacheData *Get( const wxString& name );

  private:
    wxArchiveFSCacheDataHash m_hash;
};

wxArchiveFSCacheData* wxArchiveFSCache::Add(
  const wxString& name,
  const wxArchiveClassFactory& factory,
  wxInputStream *stream ) {
  wxArchiveFSCacheData& data = m_hash[name];
  if( stream->IsSeekable() ) {
    data = wxArchiveFSCacheData( factory, stream );
  } else
  { data = wxArchiveFSCacheData( factory, wxBackingFile( stream ) ); }
  return &data;
}

wxArchiveFSCacheData *wxArchiveFSCache::Get( const wxString& name ) {
  wxArchiveFSCacheDataHash::iterator it;
  if( ( it = m_hash.find( name ) ) != m_hash.end() ) {
    return &it->second;
  }
  return NULL;
}

//----------------------------------------------------------------------------
// wxArchiveFSHandler
//----------------------------------------------------------------------------

IMPLEMENT_DYNAMIC_CLASS( wxArchiveFSHandler, wxFileSystemHandler )

wxArchiveFSHandler::wxArchiveFSHandler()
  :  wxFileSystemHandler() {
  m_Archive = NULL;
  m_FindEntry = NULL;
  m_ZipFile = m_Pattern = m_BaseDir = wxEmptyString;
  m_AllowDirs = m_AllowFiles = true;
  m_DirsFound = NULL;
  m_cache = NULL;
}

wxArchiveFSHandler::~wxArchiveFSHandler() {
  Cleanup();
  delete m_cache;
}

void wxArchiveFSHandler::Cleanup() {
  wxDELETE( m_DirsFound );
}

bool wxArchiveFSHandler::CanOpen( const wxString& location ) {
  wxString p = GetProtocol( location );
  return wxArchiveClassFactory::Find( p ) != NULL;
}

wxFSFile* wxArchiveFSHandler::OpenFile(
  wxFileSystem& fs ,
  const wxString& location ) {
  wxString right = GetRightLocation( location );
  wxString left = GetLeftLocation( location );
  wxString protocol = GetProtocol( location );
  wxString key = left + wxT( "#" ) + protocol + wxT( ":" );
  if( right.Contains( wxT( "./" ) ) ) {
    if( right.GetChar( 0 ) != wxT( '/' ) ) {
      right = wxT( '/' ) + right;
    }
    wxFileName rightPart( right, wxPATH_UNIX );
    rightPart.Normalize( wxPATH_NORM_DOTS, wxT( "/" ), wxPATH_UNIX );
    right = rightPart.GetFullPath( wxPATH_UNIX );
  }
  if( right.GetChar( 0 ) == wxT( '/' ) ) {
    right = right.Mid( 1 );
  }
  if( !m_cache ) {
    m_cache = new wxArchiveFSCache;
  }
  const wxArchiveClassFactory *factory;
  factory = wxArchiveClassFactory::Find( protocol );
  if( !factory ) {
    return NULL;
  }
  wxArchiveFSCacheData *cached = m_cache->Get( key );
  if( !cached ) {
    wxFSFile *leftFile = m_fs.OpenFile( left );
    if( !leftFile ) {
      return NULL;
    }
    cached = m_cache->Add( key, *factory, leftFile->DetachStream() );
    delete leftFile;
  }
  wxArchiveEntry *entry = cached->Get( right );
  if( !entry ) {
    return NULL;
  }
  wxInputStream *leftStream = cached->NewStream();
  if( !leftStream ) {
    wxFSFile *leftFile = m_fs.OpenFile( left );
    if( !leftFile ) {
      return NULL;
    }
    leftStream = leftFile->DetachStream();
    delete leftFile;
  }
  wxArchiveInputStream *s = factory->NewStream( leftStream );
  s->OpenEntry( *entry );
  if( s && s->IsOk() ) {
    if( factory->IsKindOf( CLASSINFO( wxZipClassFactory ) ) ) {
      ( ( wxZipInputStream* )s )->m_allowSeeking = true;
    }
    return new wxFSFile( s,
                         key + right,
                         GetMimeTypeFromExt( location ),
                         GetAnchor( location )
                         #if wxUSE_DATETIME
                         , entry->GetDateTime()
                         #endif // wxUSE_DATETIME
                       );
  }
  delete s;
  return NULL;
}

wxString wxArchiveFSHandler::FindFirst( const wxString& spec, int flags ) {
  wxString right = GetRightLocation( spec );
  wxString left = GetLeftLocation( spec );
  wxString protocol = GetProtocol( spec );
  wxString key = left + wxT( "#" ) + protocol + wxT( ":" );
  if( !right.empty() && right.Last() == wxT( '/' ) ) {
    right.RemoveLast();
  }
  if( !m_cache ) {
    m_cache = new wxArchiveFSCache;
  }
  const wxArchiveClassFactory *factory;
  factory = wxArchiveClassFactory::Find( protocol );
  if( !factory ) {
    return wxEmptyString;
  }
  m_Archive = m_cache->Get( key );
  if( !m_Archive ) {
    wxFSFile *leftFile = m_fs.OpenFile( left );
    if( !leftFile ) {
      return wxEmptyString;
    }
    m_Archive = m_cache->Add( key, *factory, leftFile->DetachStream() );
    delete leftFile;
  }
  m_FindEntry = NULL;
  switch( flags ) {
    case wxFILE:
      m_AllowDirs = false, m_AllowFiles = true;
      break;
    case wxDIR:
      m_AllowDirs = true, m_AllowFiles = false;
      break;
    default:
      m_AllowDirs = m_AllowFiles = true;
      break;
  }
  m_ZipFile = key;
  m_Pattern = right.AfterLast( wxT( '/' ) );
  m_BaseDir = right.BeforeLast( wxT( '/' ) );
  if( m_BaseDir.StartsWith( wxT( "/" ) ) ) {
    m_BaseDir = m_BaseDir.Mid( 1 );
  }
  if( m_Archive ) {
    if( m_AllowDirs ) {
      delete m_DirsFound;
      m_DirsFound = new wxArchiveFilenameHashMap();
      if( right.empty() ) // allow "/" to match the archive root
      { return spec; }
    }
    return DoFind();
  }
  return wxEmptyString;
}

wxString wxArchiveFSHandler::FindNext() {
  if( !m_Archive ) {
    return wxEmptyString;
  }
  return DoFind();
}

wxString wxArchiveFSHandler::DoFind() {
  wxString namestr, dir, filename;
  wxString match = wxEmptyString;
  while( match == wxEmptyString ) {
    m_FindEntry = m_Archive->GetNext( m_FindEntry );
    if( !m_FindEntry ) {
      m_Archive = NULL;
      m_FindEntry = NULL;
      break;
    }
    namestr = m_FindEntry->entry->GetName( wxPATH_UNIX );
    if( m_AllowDirs ) {
      dir = namestr.BeforeLast( wxT( '/' ) );
      while( !dir.empty() ) {
        if( m_DirsFound->find( dir ) == m_DirsFound->end() ) {
          ( *m_DirsFound )[dir] = 1;
          filename = dir.AfterLast( wxT( '/' ) );
          dir = dir.BeforeLast( wxT( '/' ) );
          if( !filename.empty() && m_BaseDir == dir &&
              wxMatchWild( m_Pattern, filename, false ) ) {
            match = m_ZipFile + dir + wxT( "/" ) + filename;
          }
        } else
        { break; }   // already tranversed
      }
    }
    filename = namestr.AfterLast( wxT( '/' ) );
    dir = namestr.BeforeLast( wxT( '/' ) );
    if( m_AllowFiles && !filename.empty() && m_BaseDir == dir &&
        wxMatchWild( m_Pattern, filename, false ) ) {
      match = m_ZipFile + namestr;
    }
  }
  return match;
}

#endif // wxUSE_FS_ARCHIVE
